Frontend Forever App
We have a mobile app for you to download and use. And you can unlock many features in the app.
Get it now
Intall Later
Run
HTML
CSS
Javascript
Output
Document
@charset "UTF-8"; @import url(https://fonts.googleapis.com/css?family=Nunito+Sans:300,400,600,700,800); *, :after, :before { box-sizing: border-box; padding: 0; margin: 0; } * { border: 0; box-sizing: border-box; margin: 0; padding: 0; } :root { --hue: 223; --sat: 20%; --gray0: hsl(0,0%,100%); --gray1: hsl(var(--hue),var(--sat),90%); --gray2: hsl(var(--hue),var(--sat),80%); --gray3: hsl(var(--hue),var(--sat),70%); --gray4: hsl(var(--hue),var(--sat),60%); --gray5: hsl(var(--hue),var(--sat),50%); --gray6: hsl(var(--hue),var(--sat),40%); --gray7: hsl(var(--hue),var(--sat),30%); --gray8: hsl(var(--hue),var(--sat),20%); --gray9: hsl(var(--hue),var(--sat),10%); --primary: hsl(var(--hue),90%,50%); --primary-t: hsla(var(--hue),90%,50%,0); --success: hsl(153,90%,40%); --danger: hsl(3,90%,60%); --neutral: var(--gray5); --trans-dur: 0.3s; --trans-timing: cubic-bezier(0.65,0,0.35,1); font-size: clamp(1rem,0.95rem + 0.25vw,1.25rem); } body, button { font: 1em/1.5 Inter, sans-serif; } body { background-color: var(--gray2); color: var(--gray9); display: flex; height: 100vh; transition: background-color var(--trans-dur), color var(--trans-dur); } button { cursor: pointer; outline: transparent; transition: background-color var(--trans-dur), box-shadow calc(var(--trans-dur) / 2), color var(--trans-dur); -webkit-appearance: none; appearance: none; -webkit-tap-highlight-color: transparent; } main { display: flex; overflow-x: hidden; padding: 1.5em 0; width: 100vw; height: 100vh; } small { font-size: 0.75em; } .icon { display: block; overflow: visible; width: 1em; height: 1em; &--success { color: var(--success); } &--danger { color: var(--danger); } &--neutral { color: var(--neutral); } } .activity { background-color: var(--gray0); border-radius: 1.5em; box-shadow: 0 0 0 1px var(--gray1) inset, 3em -1.5em 1.25em hsla(var(--hue),var(--sat),10%,0.2); color: var(--gray6); margin: auto; width: 17em; height: 23.5em; transition: background-color var(--trans-dur), box-shadow var(--trans-dur), color var(--trans-dur); &__body { display: grid; gap: 1.5em; padding: 1.25em 1.5em; } &__button { background-color: transparent; border-radius: 0.375em; box-shadow: 0 0 0 3px var(--primary-t); color: var(--gray6); display: flex; width: 2.25em; height: 2.25em; &:focus-visible { box-shadow: 0 0 0 3px var(--primary); } &:hover { background-color: var(--gray1); } .icon { margin: auto; width: 1.5em; height: 1.5em; } } &__change { color: var(--gray9); font-weight: 500; display: flex; align-items: center; gap: 0.375em; transition: color var(--trans-dur); .icon { width: 1.25em; height: 1.25em; } [dir="rtl"] & .icon { transform: scaleX(-1); } } &__container { overflow: hidden; } &__graph { display: flex; justify-content: space-between; margin: 0 -0.625em; &-bar { --bar-trans-dur: calc(var(--trans-dur) * 2); position: relative; text-align: center; width: 2em; &-inner, &-outer { position: relative; } &-inner { background-color: var(--gray1); border-radius: 0.375em; margin: 0 auto 0.125em; overflow: hidden; width: 0.75em; transition: background-color var(--trans-dur); } &-knob { background-color: var(--gray0); border-radius: 50%; filter: saturate(5); margin: 0.125em; position: absolute; top: 0; left: 0; width: 0.5em; height: 0.5em; transition: background-color calc(var(--trans-dur) / 2) var(--trans-timing), transform var(--trans-dur) var(--trans-timing); &:focus-visible, &:hover { background-color: var(--gray0) !important; transform: scale(1.333); } &-track { position: absolute; inset: 0; } } &-knob-track, &-svg { transition: transform var(--bar-trans-dur) var(--trans-timing); } &-svg { display: block; width: 100%; height: auto; } } &-empty { display: grid; place-items: center; width: 100%; height: 9em; } &-label { font-size: 0.625em; } } &__header { box-shadow: 0 1px 0 var(--gray1); display: flex; justify-content: space-between; align-items: center; padding: 0.5625em 1.5em; padding-inline-end: 1.125em; transition: box-shadow var(--trans-dur); } &__summary { display: grid; grid-template-columns: 1.5fr 1fr; &-end { display: flex; flex-direction: column; align-items: flex-end; } &-start { line-height: 1.25; } } &__tip { background-color: var(--gray1); box-shadow: 0 0 0 1px var(--gray2), 0 0 0.25em hsla(var(--hue),var(--sat),10%,0.2); color: var(--gray9); position: absolute; top: -0.25em; left: 50%; padding: 0 0.5em; transform: translate(-50%,-100%); transition: background-color var(--trans-dur), box-shadow var(--trans-dur), color var(--trans-dur); white-space: nowrap; &-wrapper { opacity: 0; pointer-events: none; position: absolute; inset: 0; transition: opacity var(--trans-dur), visibility var(--trans-dur), transform var(--bar-trans-dur) var(--trans-timing); visibility: hidden; z-index: 1; } } &__graph-bar:first-child &__tip { transform: translate(0,-100%); [dir="rtl"] & { transform: translate(-100%,-100%); } } &__graph-bar:last-child &__tip { transform: translate(-100%,-100%); [dir="rtl"] & { transform: translate(0,-100%); } } &__graph-bar-inner:has(&__graph-bar-knob:focus-visible) ~ &__tip-wrapper, &__graph-bar-inner:has(&__graph-bar-knob:hover) ~ &__tip-wrapper { opacity: 1; visibility: visible; } &__value { color: var(--gray9); font-size: 2.5em; line-height: 1.2; transition: color var(--trans-dur); } } /* Dark theme */ @media (prefers-color-scheme: dark) { body { background-color: var(--gray9); color: var(--gray1); } .activity { background-color: var(--gray8); box-shadow: 0 0 0 1px var(--gray7) inset, 3em -1.5em 1.25em hsla(var(--hue),var(--sat),0%,0.4); color: var(--gray3); &__button { color: var(--gray3); &:hover { background-color: var(--gray7); } } &__change, &__value { color: var(--gray1); } &__graph { &-bar { &-inner { background-color: var(--gray7); } } } &__header { box-shadow: 0 1px 0 var(--gray7); } &__tip { background-color: var(--gray9); box-shadow: 0 0 0 1px var(--gray7), 0 0 0.25em hsla(var(--hue),var(--sat),10%,0.4); color: var(--gray1); } } }
console.log("Event Fired") import React, { StrictMode, useEffect, useRef, useState } from "https://esm.sh/react"; import { createRoot } from "https://esm.sh/react-dom/client"; function fakeData() { const items: Activity[] = []; const minViewers = 7000; const maxViewers = 50000; for (let i = 0; i < 7; ++i) { const today = new Date(); const date = today.setDate(today.getDate() - i); // put earliest items first items.unshift({ date: new Date(date), viewers: Math.round(random() * (maxViewers - minViewers)) + minViewers }); } return items; } function random() { return crypto.getRandomValues(new Uint32Array(1))[0] / 2**32; } createRoot(document.getElementById("root")!).render(
); function ActivityCard({ data }: ActivityCardProps) { // display viewers for the last 7 days const viewersOfWeek = data.slice(-7); const mostViewers = Math.max(...viewersOfWeek.map(item => item.viewers)); // round max value to the next 10,000 const viewersMax = Math.ceil(mostViewers / 1e4) * 1e4; let [groupA, groupB] = viewersOfWeek.slice(-2).reverse(); if (!groupA) groupA = { viewers: 0 }; if (!groupB) groupB = { viewers: 0 }; const shortDateFormat = new Intl.DateTimeFormat(LOCALE, { month: "2-digit", day: "numeric" }); return (
{ viewersOfWeek.length ? viewersOfWeek.map((item,i) => { const { date, viewers } = item; const shortDate = shortDateFormat.format(date); return (
); }) :
No data
}
); } function ActivityCardBarGraph({ value, max, label }: ActivityCardBarGraphProps) { const [animated, setAnimated] = useState(false); const animationRef = useRef(0); const fraction = value / max; const fractionInvert = 1 - fraction; // percents should be in ascending order const firstLevel = { hue: 3, percent: 0 }; const levelMap = [ firstLevel, { hue: 33, percent: 0.2 }, { hue: 253, percent: 0.4 }, { hue: 223, percent: 0.6 }, { hue: 193, percent: 0.8 } ]; const level = levelMap.reverse().find(item => fraction > item.percent) || firstLevel; const { hue } = level; // knob positioning const knobWidth = 0.75; const knobStyle = { backgroundColor: `hsl(${hue},90%,90%)` }; const translateStart = `translateY(calc(100% - ${knobWidth}em))`; const translateEnd = `translateY(calc(${fractionInvert * 100}% - ${fractionInvert * knobWidth}em))`; const knobTrackStyle = { transform: animated ? translateEnd : translateStart }; // tooltip const valueFormatted = new Intl.NumberFormat(LOCALE).format(value); const tipHash = Math.round(random() * 0xffff).toString(16); useEffect(() => { // allow the animation to run on mount animationRef.current = setTimeout(() => setAnimated(true),0); }, []) return (
{valueFormatted}
{label}
); } function ActivityCardHeader({ title }: ActivityCardHeaderProps) { return (
{title}
); } function ActivityCardBarSummary({ groupA, groupB }: ActivityCardSummaryProps) { let change = groupA / groupB - 1; if (change === Infinity) change = 1; else if (isNaN(change)) change = 0; const groupAFormatted = Intl.NumberFormat(LOCALE, { notation: "compact" }).format(groupA); const changeIsLess = groupA < groupB; const noChange = groupA === groupB; const changeFormatted = Intl.NumberFormat(LOCALE, { maximumFractionDigits: 1, style: "percent" }).format(Math.abs(change)); const arrowDirection = changeIsLess ? "down-right" : (noChange ? "right" : "up-right"); const iconColor = changeIsLess ? "danger" : (noChange ? "neutral" : "success"); return (
{groupAFormatted}
People watched your videos today
{changeFormatted}
vs last day
); } function Icon({ icon, color }: IconProps) { const colorClass = color ? ` icon--${color}` : ""; return (
); } function IconSprites() { return (
); } const LOCALE = "en-US"; interface Activity { date?: Date; viewers: number; } type ActivityCardProps = { data: Activity[]; }; type ActivityCardBarGraphProps = { value: number; max: number; label: string; }; type ActivityCardHeaderProps = { title: string; }; type ActivityCardSummaryProps = { groupA: number; groupB: number; }; type IconProps = { icon: string; color?: string; };